package com.gitee.wsl.common.net.server.webdav

import android.net.Uri
import android.util.Log
import com.gitee.wsl.jvm.net.server.HttpCallHandler
import com.gitee.wsl.jvm.net.server.IHTTPSession
import com.gitee.wsl.jvm.net.server.IStatus
import com.gitee.wsl.jvm.net.server.Response
import com.gitee.wsl.jvm.net.server.Response.Companion.badRequest
import com.gitee.wsl.jvm.net.server.Response.Companion.created
import com.gitee.wsl.jvm.net.server.Response.Companion.forbidden
import com.gitee.wsl.jvm.net.server.Response.Companion.internalError
import com.gitee.wsl.jvm.net.server.Response.Companion.newFixedLengthResponse
import com.gitee.wsl.jvm.net.server.Response.Companion.noContent
import com.gitee.wsl.jvm.net.server.Response.Companion.notAllowed
import com.gitee.wsl.jvm.net.server.Response.Companion.notFound
import com.gitee.wsl.jvm.net.server.Response.Companion.notImplementedResponse
import com.gitee.wsl.jvm.net.server.Response.Companion.okXml
import com.gitee.wsl.jvm.net.server.Response.Companion.preconditionFailed
import com.gitee.wsl.jvm.net.server.Status
import com.gitee.wsl.jvm.net.server.sampleHttpCallHandler
import com.gitee.wsl.jvm.net.server.sampleHttpRouter
import com.gitee.wsl.struct.router.Method
import com.gitee.wsl.struct.router.UriResource
import io.github.x0b.safdav.file.FileAccessError
import io.github.x0b.safdav.file.ItemAccess
import io.github.x0b.safdav.file.ItemExistsException
import io.github.x0b.safdav.file.ItemNotFoundException
import io.github.x0b.safdav.file.SafConstants
import io.github.x0b.safdav.file.SafException
import io.github.x0b.safdav.file.SafItem
import io.github.x0b.safdav.saf.ProviderPaths
import io.github.x0b.safdav.xml.XmlResponseSerialization
import timber.log.Timber
import java.io.InputStream

fun sampleWebDav(intelWebDav: IntelWebDav)= sampleHttpRouter("webdav"){
    get("/", sampleHttpCallHandler { context, _ -> intelWebDav.onGet(context) })
    router(Method.MKCOL,"/", handler = sampleHttpCallHandler { context, _ -> intelWebDav.onMkCol(context) })
    router(Method.PROPFIND,"/", handler = sampleHttpCallHandler { context, _ -> intelWebDav.onPropFind(context) })
    router(Method.PROPPATCH,"/", handler = sampleHttpCallHandler { context, _ -> intelWebDav.onPropPatch(context) })
    router(Method.MOVE,"/", handler = sampleHttpCallHandler { context, _ -> intelWebDav.onMove(context) })
    router(Method.PUT,"/", handler = sampleHttpCallHandler { context, _ -> intelWebDav.onPut(context) })
    router(Method.DELETE,"/", handler = sampleHttpCallHandler { context, _ -> intelWebDav.onDelete(context) })
    router(Method.COPY,"/", handler = sampleHttpCallHandler { context, _ -> intelWebDav.onCopy(context) })
}

class IntelWebDav(
    val itemAccess:ItemAccess<SafItem>,
    val respSerializer: XmlResponseSerialization,
    val paths:ProviderPaths,
    val requiredAuthHeader:String
    ){

    /**
     * Retrieve an entity
     * @param session
     * @return <table>
     * <tr>
     * <td>200</td>
     * <td>OK</td>
    </tr> *
     * <tr>
     * <td>400</td>
     * <td>Bad Request</td>
    </tr> *
    </table> *
     */
    fun onGet(session: IHTTPSession): Response {
        val requestUri: String = session.uri
        val uri: Uri = paths.getUriByMappedPath(requestUri)
        var file: SafItem? = null
        file = try {
            itemAccess.readMeta(uri)
        } catch (e: ItemNotFoundException) {
            return notFound("onGet")
        }
        val stream: InputStream? = itemAccess.readFile(file!!)
        return if (null != stream) {
            newFixedLengthResponse(Status.OK, file.contentType, stream, file.getLength())
        } else badRequest("onGet")
    }

    /**
     * Retrieve Properties.
     * @param session
     * @return
     */
     fun onPropFind(session: IHTTPSession): Response {
        val hDepth: String? = session.headers.get("depth")
        val depth: Int = try {
            hDepth?.toInt()?:1
        } catch (e: NumberFormatException) {
            Timber.w("Invalid depth header, assuming `1´")
            1
        }

        // if root, offer permissions; else, normalize (strip permission from) path
        val path: String = session.uri
        Timber.d("onPropFind: PROPFIND $path")
        val directory: SafItem = if ("/" == path) {
            paths.getSafRootDirectory()
        } else {
            try {
                val uri: Uri = paths.getUriByMappedPath(path)
                itemAccess.readMeta(uri)
            } catch (e: ItemNotFoundException) {
                return notFound("onPropFind")
            } catch (e: FileAccessError) {
                return internalError("onPropFind")
            }
        }
        var requestUri: String = session.uri
        if (!requestUri.contains(SafConstants.SAF_REMOTE_URL)) {
            requestUri = SafConstants.SAF_REMOTE_URL + requestUri.substring(1)
        }
        val responseXml: String = respSerializer.propFindSerialize(directory, requestUri)
        return okXml(responseXml, "onPropFind")
    }

    /**
     * Change Properties of an item
     * @param session - the incoming HTTP request
     * @return - an appropriate HTTP response
     */
    fun onPropPatch(session: IHTTPSession): Response {
        return notImplementedResponse(session, "onPropPatch")
    }

    /**
     * Create a folder
     * @param session the incoming HTTP request
     * @return <table>
     * <tr>
     * <td>201</td>
     * <td>Folder successfully created</td>
    </tr> *
     * <tr>
     * <td>403</td>
     * <td>Folder is not accessible</td>
    </tr> *
     * <tr>
     * <td>405</td>
     * <td>Folder already exists</td>
    </tr> *
    </table> *
     */
    fun onMkCol(session: IHTTPSession): Response {
        val path: String = session.uri
        Timber.d("onMkCol: MKCOL $path")
        val uri: Uri = try {
            paths.getUriByMappedPath(path)
        } catch (e: ItemNotFoundException) {
            return forbidden("onMkCol")
        }
        try {
           itemAccess.createDirectory(uri)
        } catch (e: ItemNotFoundException) {
            return notFound("onMkCol")
        } catch (e: ItemExistsException) {
            return notAllowed("onMkCol")
        } catch (e: SecurityException) {
            return forbidden("onMkCol")
        } catch (e: IllegalStateException) {
            return forbidden("onMkCol")
        }
        return created("onMkCol")
    }

    /**
     * Delete an Entity
     * @param session the incoming HTTP request
     * @return <table>
     * <tr>
     * <td>204</td>
     * <td>File deleted</td>
    </tr> *
     * <tr>
     * <td>403</td>
     * <td>Folder is not accessible</td>
    </tr> *
     * <tr>
     * <td>404</td>
     * <td>File not found</td>
     *
    </tr> * <tr>
     * <td>501</td>
     * <td>File Access Error</td>
    </tr> *
    </table> *
     */
    fun onDelete(session: IHTTPSession): Response {
        val path: String = session.uri
        
        Timber.d("onDelete: DELETE $path")
        val uri: Uri = try {
            paths.getUriByMappedPath(path)
        } catch (e: ItemNotFoundException) {
            return forbidden("onDelete")
        }
        try {
            itemAccess.deleteItem(uri)
        } catch (e: FileAccessError) {
            return internalError("onDelete")
        } catch (e: ItemNotFoundException) {
            return notFound("onDelete")
        }
        return noContent("onDelete")
    }

    /**
     * Replace an Entity
     * @param session
     * @return
     */
    fun onPut(session: IHTTPSession): Response {
        val path: String = session.uri
        var mime: String? = session.headers["content-type"]
        
        val lengthValue: String = session.headers["content-length"]?:return badRequest("onPut")
        Timber.d("onPut: PUT $path; $mime; $lengthValue byte")
        val `is`: InputStream = session.inputStream?:return internalError("onPut")
        val length: Long = try {
            lengthValue.toLong()
        } catch (e: NumberFormatException) {
            return badRequest("onPut")
        }
        if (null == mime) {
            mime = "application/octet-stream"
        }
        val uri: Uri = try {
            paths.getUriByMappedPath(path)
        } catch (e: ItemNotFoundException) {
            return forbidden("onPut")
        }
        var item: SafItem
        val etag: String
        try {
            val name = path.substring(path.lastIndexOf('/') + 1)
            try {
                item = itemAccess.readMeta(uri)
                itemAccess.writeFile(uri, `is`, length)
            } catch (e: ItemNotFoundException) {
                item = itemAccess.createFile(uri, name, mime, `is`, length)
                etag = "\"" + item.eTag + "\""
                val response: Response = created("onPut")
                response.addHeader("etag", etag)
                return response
            }
        } catch (e: SafException) {
            return internalError("onPut")
        }
        etag = "\"" + item.eTag + "\""
        val response: Response = created("onPut")
        response.addHeader("etag", etag)
        return response
    }

    /**
     * Copy an Entity. Only works within the server managed space.
     * @param session
     * @return
     */
    fun onCopy(session: IHTTPSession): Response {
        return notImplementedResponse(session, "onCopy")
    }

    /**
     * Move an Entity. Depending on the file location, this may be a rename or implemented using copy.
     * @param session
     * @return
     */
     fun onMove(session: IHTTPSession): Response {
        val path: String = session.uri
        val src: Uri = paths.getUriByMappedPath(path)
        val noOverwrite = "F" == session.headers.get("overwrite")
        val destination: String = session.headers.get("destination")
            ?: return badRequest("onMove")
        val dst: Uri =
            paths.getUriByMappedPath(destination.substring(SafConstants.SAF_REMOTE_URL.length - 1))
        var exists = true
        try {
            itemAccess.readMeta(dst)
        } catch (e: ItemNotFoundException) {
            exists = false
        }
        if (noOverwrite && exists) {
            return preconditionFailed("onMove")
        }
        itemAccess.moveItem(src, dst)
        return if (exists) {
            noContent("onMove")
        } else {
            created("onMove")
        }
    }
}